/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.polarrose.wsf.controller;

import com.polarrose.json.JsonSerializer;
import com.polarrose.wsf.WebServiceAccount;
import com.polarrose.wsf.WebServiceAccountManager;
import com.polarrose.wsf.WebServiceError;
import com.polarrose.wsf.action.ActionHandlerException;
import com.polarrose.wsf.action.WebServiceActionContext;
import com.polarrose.wsf.action.WebServiceActionHandler;
import com.polarrose.wsf.action.PublicWebServiceActionHandler;
import com.polarrose.wsf.annotation.ValidatorParameter;
import com.polarrose.wsf.annotation.ValidatorParameters;
import com.polarrose.wsf.annotation.WebServiceParameter;
import com.polarrose.wsf.converter.BigDecimalConverter;
import com.polarrose.wsf.converter.Converter;
import com.polarrose.wsf.converter.ConverterException;
import com.polarrose.wsf.converter.UuidConverter;
import com.polarrose.wsf.stats.CallStatistics;
import com.polarrose.wsf.stats.StatisticsCollector;
import com.polarrose.wsf.validator.Validator;
import com.polarrose.wsf.validator.ValidatorException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.list.GrowthList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;

// TODO This class is too fat. See what can be moved to external utility classes.

public abstract class AbstractWebServiceController implements Controller
{
	static final Log log = LogFactory.getLog(WebServiceController.class);

    private static final Map<Class, Converter> converters = new HashMap<Class, Converter>();

    static {
        converters.put(UUID.class, new UuidConverter());
        converters.put(BigDecimal.class, new BigDecimalConverter());
    }

    private final StatisticsCollector statsCollector = new StatisticsCollector();

    //public abstract void initializeActions();

    protected abstract WebServiceActionHandler getActionByNameAndVersion(String name, String version);

    //
    
    private String prependedComment;

	public void setPrependedComment(String prependedComment) {
		this.prependedComment = prependedComment;
	}

	private WebServiceAccountManager accountManager;

	public void setAccountManager(WebServiceAccountManager accountManager) {
		this.accountManager = accountManager;
	}

	//

    private static final WebServiceError INTERNAL_ERROR = new WebServiceError("InternalError",
        "There is an internal problem, which you cannot resolve. Please retry the request later.");

    //

    /**
	 * Thread local to store the current request id.
	 */

	private static final ThreadLocal<String> currentRequestId = new ThreadLocal<String>();

	/**
	 * Return the current request id. The request id is a unique string.
	 *
	 * @return the current request id.
	 */

	private static String getCurrentRequestId() {
		return currentRequestId.get();
	}

	/**
	 * Global JSON serializer used to convert responses to JSON.
	 */

	private final JsonSerializer jsonSerializer = new JsonSerializer();

	//

    /**
	 * @see org.springframework.web.servlet.mvc.Controller#handleRequest(javax.servlet.http.HttpServletRequest,
	 *      javax.servlet.http.HttpServletResponse)
	 */

	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
		String requestId = UUID.randomUUID().toString();

		if (log.isDebugEnabled()) {
			log.debug("Processing new request: " + requestId);
		}
		currentRequestId.set(requestId);

		long t0 = System.currentTimeMillis(), t1;
		boolean wasSuccess = false;
		try
        {
            // Check basic request structure and see if we can find the action handler
            
            WebServiceActionHandler actionHandler = checkBasicRequestStructure(request);

            // If the actionHandler requires authorization then we check that

            WebServiceAccount account = checkAuthorization(request, actionHandler);

            // Handle a statistics request
            // TODO Move this to it's own handler instead

            if (handleStatisticsRequest(request, response)) {
				wasSuccess = true;
				return null;
			}

            //

            Object parameters = instantiateParameters(request, actionHandler);
            if (parameters != null) {
                transmogrifyParameters(request, parameters);
            }

            //

            WebServiceActionContext context = new WebServiceActionContext(request, getCurrentRequestId(), account);
            Object actionHandlerExecutionResult = executeActionHandler(actionHandler, context, parameters);

            writeSuccessResponse(request, response, actionHandlerExecutionResult);
			wasSuccess = true;
		} catch (ActionHandlerException e) {
			if (log.isDebugEnabled()) {
				log.debug("Writing error response: " + requestId);
			}
			writeErrorResponse(request, response, e.getErrors());
		} finally {
			if (log.isDebugEnabled()) {
				log.debug("Finished processing request: " + requestId);
			}
			t1 = System.currentTimeMillis();

			statsCollector.incrementCounter(request.getParameter("Action"), wasSuccess, t1 - t0);
		}

		return null;
	}

	private Object executeActionHandler(WebServiceActionHandler actionHandler, WebServiceActionContext webServiceActionContext, Object parameters)
        throws ActionHandlerException
    {
		try {
			return actionHandler.execute(webServiceActionContext, parameters);
		} catch (ActionHandlerException e) {
			throw e;
		} catch (Throwable t) {
			if (log.isErrorEnabled()) {
				log.error("Cannot execute the ActionHandler", t);
			}
			throw new ActionHandlerException(INTERNAL_ERROR);
		}
	}

	private void transmogrifyParameters(HttpServletRequest request, Object parameters)
        throws ActionHandlerException, IntrospectionException, IllegalAccessException, InstantiationException
    {
		List<WebServiceError> errors = new ArrayList<WebServiceError>();

		BeanInfo beanInfo = Introspector.getBeanInfo(parameters.getClass());

		for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
			Method setter = propertyDescriptor.getWriteMethod();
			if (setter != null && setter.isAnnotationPresent(WebServiceParameter.class)) {
				Object parameterValue;

				if (isListType(propertyDescriptor.getPropertyType())) {
					parameterValue = transmogrifyListType(request, propertyDescriptor, parameters);
				} else {
					parameterValue = transmogrifySimpleType(request, propertyDescriptor, parameters);
				}

				if (parameterValue != null) {
					try {
						propertyDescriptor.getWriteMethod().invoke(parameters, parameterValue);
					} catch (Throwable t) {
						if (log.isErrorEnabled()) {
							log.error("Cannot set parameter " + propertyDescriptor.getName() + " on parameters class " + parameters.getClass().getName(), t);
						}
						throw new ActionHandlerException(INTERNAL_ERROR);
					}
				}
			}
		}

		if (errors.size() != 0) {
			throw new ActionHandlerException(errors);
		}
	}

	private Object transmogrifySimpleType(HttpServletRequest request, PropertyDescriptor propertyDescriptor, Object parameters)
        throws ActionHandlerException, IllegalAccessException, InstantiationException
    {
		Object value = null;

		Method setter = propertyDescriptor.getWriteMethod();

		WebServiceParameter webServiceParameter = setter.getAnnotation(WebServiceParameter.class);
		String parameterName = StringUtils.capitalize(propertyDescriptor.getName());

		String parameterValue = request.getParameter(parameterName);
		if (parameterValue != null) {
			// Do initial conversion from string to the expected type

			value = convertParameter(parameterName, parameterValue, propertyDescriptor.getPropertyType(), parameters.getClass());

			// Validate

			if (webServiceParameter.validator() != Validator.class) {
				Class<? extends Validator> validatorClass = webServiceParameter.validator();
				Validator validator = validatorClass.newInstance();
				Annotation[] annotations = setter.getAnnotations();
				for (Annotation annotation : annotations) {
					if (ValidatorParameter.class.isAssignableFrom(annotation.getClass())) {
						applyValidatorParameter(validatorClass, validator, annotation);
					}
					if (ValidatorParameters.class.isAssignableFrom(annotation.getClass())) {
						ValidatorParameters p = (ValidatorParameters) annotation;
						ValidatorParameter[] validatorParameters = p.parameters();
						for (ValidatorParameter validatorParameter : validatorParameters) {
							applyValidatorParameter(validatorClass, validator, validatorParameter);
						}
					}
				}
				try {
                    //noinspection unchecked
                    validator.validate(value);
                } catch (ValidatorException e) {
                    List<WebServiceError> webServiceErrors = new ArrayList<WebServiceError>(e.getValidationErrors().size());
                    for (String validationError : e.getValidationErrors()) {
                        webServiceErrors.add(new WebServiceError("InvalidParameterValue", "The value of the '" + parameterName + "' parameter is invalid: " + validationError));
                    }
                    throw new ActionHandlerException(webServiceErrors);
				} catch (Throwable t) {
					if (log.isErrorEnabled()) {
						log.error("Cannot validate value of parameter " + parameterName + " of parameter class " + parameters.getClass().getName() + " using validator " + validatorClass.getName(), t);
					}
					throw new ActionHandlerException(INTERNAL_ERROR);
				}
			}
		}

		return value;
	}

	private Object transmogrifyListType(HttpServletRequest request, PropertyDescriptor propertyDescriptor, Object actionHandler)
        throws ActionHandlerException, IllegalAccessException, InstantiationException {
		Method setter = propertyDescriptor.getWriteMethod();

		WebServiceParameter webServiceParameter = setter.getAnnotation(WebServiceParameter.class);
		String parameterName = StringUtils.capitalize(propertyDescriptor.getName());

		List<Object> list = GrowthList.decorate(new ArrayList<Object>());

		Enumeration parameterNames = request.getParameterNames();
		while (parameterNames.hasMoreElements()) {
			String paramName = (String) parameterNames.nextElement();

			if (paramName.startsWith(parameterName + ".")) {
				String parameterValue = request.getParameter(paramName);
				String paramIdx = paramName.substring(paramName.indexOf('.') + 1);
				if (paramIdx.indexOf('.') >= 0) {
					paramIdx = paramIdx.substring(0, paramIdx.indexOf('.'));
				}
				Integer idx = Integer.valueOf(paramIdx);

				// Do initial conversion from string to the expected type

				Object convertedValue;
				try {
					Class<?> componentType = getComponentType(propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0], 0);
					if (simpleTypes.contains(componentType) || autoboxTypes.containsKey(componentType)) {
						convertedValue = convertParameter(paramName, parameterValue, componentType, actionHandler.getClass());
						list.set(idx, convertedValue);
					} else if (converters.containsKey(componentType)) {
                        // TODO Make this more fault tolerant
                        convertedValue = converters.get(componentType).convert(parameterValue);
                        list.set(idx, convertedValue);
                    } else {
						// Java Bean
						// ensure bean exists in the list
						if (idx >= list.size()) {
							list.set(idx, componentType.newInstance());
						} else {
							Object object = list.get(idx);
							if (object == null) {
								list.set(idx, componentType.newInstance());
							}
						}
						// object exists, lets update it's field
						Object object = list.get(idx);
						BeanInfo innerBeanInfo = Introspector.getBeanInfo(componentType);
						PropertyDescriptor[] propertyDescriptors = innerBeanInfo.getPropertyDescriptors();
						PropertyDescriptor propDesc = findPropertyDescriptorForName(paramName.substring(paramName.lastIndexOf('.') + 1), propertyDescriptors);
						convertedValue = convertParameter(paramName, parameterValue, propDesc.getPropertyType(), actionHandler.getClass());
						propDesc.getWriteMethod().invoke(object, convertedValue);
					}
				} catch (Throwable t) {
					if (log.isErrorEnabled()) {
						log.error("Cannot convert value of parameter " + parameterName + " of ActionHandler " + actionHandler.getClass().getName() + " to type "
								+ propertyDescriptor.getPropertyType().getSimpleName(), t);
					}
					throw new ActionHandlerException(new WebServiceError("InvalidParameterValue", "The value of the '" + parameterName
							+ "' parameter is invalid. Expected a valid TODO String|Integer|Double| TODO"));
				}

			}
		}

		// Validate

		if (webServiceParameter.validator() != Validator.class) {
			Class<? extends Validator> validatorClass = webServiceParameter.validator();
			Validator validator = validatorClass.newInstance();
			Annotation[] annotations = setter.getAnnotations();
			for (Annotation annotation : annotations) {
				if (ValidatorParameter.class.isAssignableFrom(annotation.getClass())) {
					applyValidatorParameter(validatorClass, validator, annotation);
				}
				if (ValidatorParameters.class.isAssignableFrom(annotation.getClass())) {
					ValidatorParameters parameters = (ValidatorParameters) annotation;
					ValidatorParameter[] validatorParameters = parameters.parameters();
					for (ValidatorParameter validatorParameter : validatorParameters) {
						applyValidatorParameter(validatorClass, validator, validatorParameter);
					}
				}
			}

			try {
                //noinspection unchecked
                validator.validate(list);
			} catch (ValidatorException e) {
                List<WebServiceError> webServiceErrors = new ArrayList<WebServiceError>(e.getValidationErrors().size());
                for (String validationError : e.getValidationErrors()) {
                    webServiceErrors.add(new WebServiceError("InvalidParameterValue", "The value of the '" + parameterName + "' parameter is invalid: " + validationError));
                }
                throw new ActionHandlerException(webServiceErrors);
            } catch (Throwable t) {
                if (log.isErrorEnabled()) {
                    log.error("Cannot validate value of parameter " + parameterName + " of ActionHandler " + actionHandler.getClass().getName() + " using validator " + validatorClass.getName(), t);
                }
				throw new ActionHandlerException(INTERNAL_ERROR);
			}
		}
		return list;
	}

	private PropertyDescriptor findPropertyDescriptorForName(String propertyName, PropertyDescriptor[] propertyDescriptors) {
		for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
			if (propertyDescriptor.getName().equals(StringUtils.uncapitalize(propertyName))) {
				return propertyDescriptor;
			}
		}
		return null;
	}

	private void applyValidatorParameter(Class<? extends Validator> validatorClass, Validator validator, Annotation annotation) throws ActionHandlerException {
		ValidatorParameter validatorParameter = (ValidatorParameter) annotation;
		String validatorProperty = validatorParameter.name();
		String validatorSetter = "set" + StringUtils.capitalize(validatorProperty); // TODO Spring has a convenience method for this
		String validatorSetterValue = validatorParameter.value();
		try {
			Method method = validatorClass.getMethod(validatorSetter, String.class);
			method.invoke(validator, validatorSetterValue);
		} catch (Exception e) {
			log.error("Cannot set validator property " + validatorProperty + " of Validator " + validatorClass.getName() + " to " + validatorSetterValue);
			throw new ActionHandlerException(new WebServiceError("InvalidValidatorConfiguration", "Cannot set validator property " + validatorProperty + " of Validator " + validatorClass.getName()
					+ " to " + validatorSetterValue));
		}
	}

	private WebServiceAccount checkAuthorization(HttpServletRequest request, WebServiceActionHandler actionHandler)
        throws ActionHandlerException
    {
		// Short circuit if no account manager has been specified or if the action is a public one

        if (accountManager == null || actionHandler instanceof PublicWebServiceActionHandler) {
            return null;
		}

		// Check if the required auth parameters are there

        List<WebServiceError> errors = new ArrayList<WebServiceError>();
        
        String[] requiredParameters = new String[] { "Signature", "SignatureVersion", "Action", "Timestamp", "AccessKey" };

		for (String requiredParameter : requiredParameters) {
			String actionName = request.getParameter(requiredParameter);
			if (actionName == null) {
				errors.add(new WebServiceError("MissingRequiredParameter", "The '" + requiredParameter + "' parameter is missing."));
			}
		}

		if (errors.size() != 0) {
			throw new ActionHandlerException(errors);
		}

        // Find the account and check if the signature matches

        WebServiceAccount account;
        
        try {
            account = accountManager.getAccountByAccessKey(request.getParameter("AccessKey"));
        } catch (Throwable t) {
            if (log.isErrorEnabled()) {
                log.error("Failure in AccountManager while lookup up account: " + t.getMessage(), t);
            }
            throw new ActionHandlerException(INTERNAL_ERROR);
        }

		if (account == null) {
			throw new ActionHandlerException(new WebServiceError("AuthFailure", "The AccessKey is unknown or disabled."));
		}

		String signature = createSignature(account, generateSignatureData(request.getParameterMap()));
		if (!signature.equals(request.getParameter("Signature"))) {
			throw new ActionHandlerException(new WebServiceError("AuthFailure", "The signature of the request is not correct."));
		}

		return account;
	}

    /**
     * Find the class of the parameters type. This is really terrible code and I'm sure it will not work in a couple
     * of cases where the Action is part of a hierarchy for example. It really needs to be replaced by a proper
     * implementation. Maybe some apache commons library has this functionality?
     *
     * @param clazz
     * @return
     */

    private Class findParametersClass(Class clazz)
    {
        if (WebServiceActionHandler.class.isAssignableFrom(clazz)) {
            for (Type type : clazz.getGenericInterfaces()) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) type;
                    Class x = (Class) parameterizedType.getRawType();
                    if (WebServiceActionHandler.class.isAssignableFrom(x)) {
                        return (Class) parameterizedType.getActualTypeArguments()[parameterizedType.getActualTypeArguments().length - 1];
                    }
                }
            }

            Type genericSuperClazz = clazz.getGenericSuperclass();
            if (genericSuperClazz != null && genericSuperClazz instanceof ParameterizedType) {
                ParameterizedType parameterizedType = (ParameterizedType) genericSuperClazz;
                Class x = (Class) parameterizedType.getRawType();
                if (WebServiceActionHandler.class.isAssignableFrom(x)) {
                    return (Class) parameterizedType.getActualTypeArguments()[parameterizedType.getActualTypeArguments().length - 1];
                }
            }
        }
        return null;
    }

    private Object instantiateParameters(HttpServletRequest request, WebServiceActionHandler actionHandler)
        throws ActionHandlerException, IntrospectionException, IllegalAccessException, InstantiationException
    {
        // Get the class for the action and extract the parameters class. This is done with Spring's AopUtils so
        // that we will get the correct class back if if it has been made @Transactional for example.

        Class actionClass = AopUtils.getTargetClass(actionHandler);
        
        Class parametersClass = findParametersClass(actionClass);
        if (parametersClass == null) {
            if (log.isErrorEnabled()) {
                log.error("Cannot find the class of the parameters for action " + actionClass.getSimpleName());
            }
            throw new ActionHandlerException(INTERNAL_ERROR);
        }

        // Short-circuit if there are no parameters 

        if (parametersClass == Void.class) {
            return null;
        }

        // Check the required parameters for this handler

		List<WebServiceError> errors = new ArrayList<WebServiceError>();

		BeanInfo beanInfo = Introspector.getBeanInfo(parametersClass);

		for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
			Method setter = propertyDescriptor.getWriteMethod();
			if (setter != null && setter.isAnnotationPresent(WebServiceParameter.class)) {
				WebServiceParameter webServiceParameter = setter.getAnnotation(WebServiceParameter.class);
				String parameterName = StringUtils.capitalize(propertyDescriptor.getName());
				if (isListType(propertyDescriptor.getPropertyType())) {
					if (!webServiceParameter.optional() && !requestContainsAtLeastOneParameterOfName(request, parameterName)) {
						errors.add(new WebServiceError("MissingRequiredParameter", "The '" + parameterName + "' multiple-value parameter is missing. It should have at least one value specified."));
					}
				} else {
					if (!webServiceParameter.optional() && request.getParameter(parameterName) == null) {
						errors.add(new WebServiceError("MissingRequiredParameter", "The '" + parameterName + "' parameter is missing."));
					}
				}
			}
		}

		if (errors.size() > 0) {
			throw new ActionHandlerException(errors);
		}

		// Instantiate the parameters

		return parametersClass.newInstance();
	}

	private boolean requestContainsAtLeastOneParameterOfName(HttpServletRequest request, String parameterNamePrefix)
    {
		Enumeration parameterNames = request.getParameterNames();
		while (parameterNames.hasMoreElements()) {
			String paramName = (String) parameterNames.nextElement();
			if (paramName.startsWith(parameterNamePrefix + ".")) {
				return true;
			}
		}
		return false;
	}

	private WebServiceActionHandler checkBasicRequestStructure(HttpServletRequest request) throws ActionHandlerException
    {
        List<WebServiceError> errors = new ArrayList<WebServiceError>();

        // Check if Action and Version parameter are present
        
        String[] requiredParameters = new String[] { "Action", "Version" };

        for (String requiredParameter : requiredParameters) {
            String actionName = request.getParameter(requiredParameter);
            if (actionName == null) {
                errors.add(new WebServiceError("MissingRequiredParameter", "The '" + requiredParameter + "' parameter is missing."));
            }
        }
        
        if (errors.size() != 0) {
            throw new ActionHandlerException(errors);
        }

		// Check if the request has parameters with multiple values, which is not allowed

		Enumeration parameterNamesEnumeration = request.getParameterNames();
		while (parameterNamesEnumeration.hasMoreElements()) {
			String parameterName = (String) parameterNamesEnumeration.nextElement();
			String[] parameterValues = request.getParameterValues(parameterName);
			if (parameterValues.length > 1) {
				errors.add(new WebServiceError("InvalidParameterValue", "Parameters cannot be specified multiple times."));
			}
		}

		if (errors.size() > 0) {
			throw new ActionHandlerException(errors);
		}

        // Return the action handler

        String actionName = request.getParameter("Action");
        String actionVersion = request.getParameter("Version");

        WebServiceActionHandler actionHandler = getActionByNameAndVersion(actionName, actionVersion);
        if (actionHandler == null) {
            throw new ActionHandlerException(new WebServiceError("UnknownAction", "Action " + actionName + " is unknown to version " + actionVersion));
        }

        return actionHandler;
    }

	private boolean isListType(Class<?> propertyType) {
		return List.class.isAssignableFrom(propertyType);
	}

	private static Class getComponentType(Object genericType, int index) {
		Class paramClass = null;

		if (genericType instanceof ParameterizedType) {
			ParameterizedType type = (ParameterizedType) genericType;
			if (type.getActualTypeArguments()[index] instanceof Class) {
				paramClass = (Class) type.getActualTypeArguments()[index];
			} else if (type.getActualTypeArguments()[index] instanceof WildcardType) {
				WildcardType wildcardType = (WildcardType) type.getActualTypeArguments()[index];
				if (wildcardType.getUpperBounds()[index] instanceof Class) {
					paramClass = (Class) wildcardType.getUpperBounds()[index];
				}
			}
		}

		return paramClass;
	}

	private void writeErrorResponse(HttpServletRequest request, HttpServletResponse response, List<WebServiceError> errors) throws IOException {
		WebServiceErrorResponse errorResponse = new WebServiceErrorResponse(getCurrentRequestId(), errors);
		response.setContentType("text/javascript");
		PrintWriter writer = response.getWriter();
		if (prependedComment != null) {
			writer.println(prependedComment);
		}
		String jsonpParameter = request.getParameter("JsonCallback");
		if (jsonpParameter != null && !"".equals(jsonpParameter.trim())) {
			writer.print(jsonpParameter);
			writer.print("(");
		}
		writer.println(jsonSerializer.serialize(errorResponse));
		if (jsonpParameter != null) {
			writer.print(")");
		}
		writer.println();
		writer.flush();
	}

	private void writeSuccessResponse(HttpServletRequest request, HttpServletResponse response, Object result) throws IOException {
		WebServiceSuccessResponse successResponse = new WebServiceSuccessResponse(getCurrentRequestId(), result);
		response.setContentType("text/javascript");
		PrintWriter writer = response.getWriter();
		if (prependedComment != null) {
			writer.println(prependedComment);
		}
		String jsonpParameter = request.getParameter("JsonCallback");
		if (jsonpParameter != null && !"".equals(jsonpParameter.trim())) {
			writer.print(jsonpParameter);
			writer.print("(");
		}
		writer.print(jsonSerializer.serialize(successResponse));
		if (jsonpParameter != null) {
			writer.print(")");
		}
		writer.println();
		writer.flush();
	}

	private boolean handleStatisticsRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
		if ("GetCallStatistics".equals(request.getParameter("Action"))) {
			Map<String, Map<String, Object>> responseObject = new HashMap<String, Map<String, Object>>();
			CallStatistics[] stats = statsCollector.getAllCallStatistics();
			for (CallStatistics callStatistics : stats) {
				Map<String, Object> values = new HashMap<String, Object>();
				values.put("averageResponseTime", callStatistics.getAverageResponseTime());
				values.put("totalInvocations", callStatistics.getTotalInvocations());
				values.put("totalFailures", callStatistics.getTotalFailures());
				responseObject.put(callStatistics.getCallName(), values);
			}
			writeSuccessResponse(request, response, responseObject);
			return true;
		}
		return false;
	}

	//

	private String generateSignatureData(Map parameters) {
		// Create a sorted list of the query parameter names

		List<String> sortedNames = new ArrayList<String>();
		for (Object name : parameters.keySet()) {
			if (!name.equals("Signature")) {
				sortedNames.add(name.toString());
			}
		}

		Collections.sort(sortedNames, String.CASE_INSENSITIVE_ORDER);

		// Concatenate the sorted name/value pairs to form the signature data

		StringBuffer signatureData = new StringBuffer();
		for (String name : sortedNames) {
			signatureData.append(name);
			String[] values = (String[]) parameters.get(name);
			signatureData.append(values[0]);
		}

		return signatureData.toString();
	}

	private final static String HMAC_SHA1 = "HmacSHA1";

	private String createSignature(WebServiceAccount account, String data) {
		try {
			// Create a Mac instance and initialize it with the AWS secret key
			SecretKeySpec secretKeySpec = new SecretKeySpec(account.getSecretKey().getBytes(), HMAC_SHA1);
			Mac mac = Mac.getInstance(HMAC_SHA1);
			mac.init(secretKeySpec);
			// Compute the HMAC and return the base64 encoded result
			byte[] rawSignature = mac.doFinal(data.getBytes());
			return new String(Base64.encodeBase64(rawSignature));
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException("Cannot get an instance of Mac " + HMAC_SHA1);
		} catch (InvalidKeyException e) {
			throw new RuntimeException("Invalid secret key: " + account.getSecretKey());
		}
	}

	//

	private static final Set<Class<?>> simpleTypes = new HashSet<Class<?>>();

	static {
		simpleTypes.add(String.class);
		simpleTypes.add(Boolean.class);
		simpleTypes.add(Byte.class);
		simpleTypes.add(Character.class);
		simpleTypes.add(Short.class);
		simpleTypes.add(Integer.class);
		simpleTypes.add(Long.class);
		simpleTypes.add(Float.class);
		simpleTypes.add(Double.class);
		simpleTypes.add(BigDecimal.class);
		simpleTypes.add(URL.class);
	}

	private static final Map<Class<?>, Class<?>> autoboxTypes = new HashMap<Class<?>, Class<?>>();

	static {
		autoboxTypes.put(int.class, Integer.class);
		autoboxTypes.put(boolean.class, Boolean.class);
		autoboxTypes.put(byte.class, Byte.class);
		autoboxTypes.put(short.class, Short.class);
		autoboxTypes.put(byte.class, Byte.class);
		autoboxTypes.put(long.class, Long.class);
		autoboxTypes.put(float.class, Float.class);
		autoboxTypes.put(double.class, Double.class);
	}

	private Object convertParameter(String name, String value, Class<?> clazz, Class<?> actionHandlerClass)
        throws ActionHandlerException
    {
        if (autoboxTypes.containsKey(clazz)) {
            clazz = autoboxTypes.get(clazz);
        }

        if (simpleTypes.contains(clazz)) {
            try {
                Constructor c = clazz.getConstructor(String.class);
                return c.newInstance(value);
            } catch (Throwable t) {
                if (log.isErrorEnabled()) {
                    log.error("Cannot create a new " + clazz.getSimpleName() + " instance: " + t.getMessage(), t);
                }
                throw new RuntimeException("Cannot create a new " + clazz.getSimpleName() + " instance: " + t.getMessage());
            }
        } else if (converters.containsKey(clazz)) {
            Converter converter = converters.get(clazz);
            try {
                return converter.convert(value);
            } catch (ConverterException e) {
                throw new ActionHandlerException(new WebServiceError("InvalidParameterValue", "The value of the '" + name + "' parameter is invalid: " + e.getMessage()));
            } catch (Throwable t) {
                if (log.isErrorEnabled()) {
                    log.error("An error occurred while converting value '" + value + "' to an instance of type " + clazz.getName() + " using converter "
                        + converter.getClass().getName(), t);
                }
                throw new ActionHandlerException(INTERNAL_ERROR);
            }
        } else {
            if (log.isErrorEnabled()) {
                log.error("Don't know how to convert to a parameter of type " + clazz.getSimpleName());
            }
            throw new ActionHandlerException(INTERNAL_ERROR);
        }
	}

	//

	public static abstract class WebServiceResponse {
		private final String webServiceCallId;

		public String getWebServiceCallId() {
			return webServiceCallId;
		}

		WebServiceResponse(String webServiceCallId) {
			this.webServiceCallId = webServiceCallId;
		}
	}

	public static class WebServiceSuccessResponse extends WebServiceResponse {
		private final Object response;

		public Object getResponse() {
			return response;
		}

		WebServiceSuccessResponse(String webServiceCallId) {
			super(webServiceCallId);
			this.response = null;
		}

		WebServiceSuccessResponse(String webServiceCallId, Object response) {
			super(webServiceCallId);
			this.response = response;
		}
	}

	public static class WebServiceErrorResponse extends WebServiceResponse {
		private final List<WebServiceError> errors;

		public List<WebServiceError> getErrors() {
			return errors;
		}

		WebServiceErrorResponse(String webServiceCallId, List<WebServiceError> errors) {
			super(webServiceCallId);
			this.errors = errors;
		}
	}
}
